import abc
import collections

import coin.proto.coin_market_enums_pb2 as enums_pb2
import coin.proto.coin_feed_pb2 as proto_pb2
from coin.exchange.base.kr_rest.product import NativeProduct
from coin.exchange.base.kr_rest.futures_product import NativeFuturesProduct


def _if_not_none_nano(x):
  if x is not None:
    return int(x * 10**9)
  else:
    return None


SidedBookMock = collections.namedtuple('SidedBookMock', ['price', 'quantity'])


class ProtoMsgAdapter(object):
  def __init__(self, book_feed):
    self.book_feed = book_feed
    self.asks = []
    self.bids = []
    for ask in self.book_feed.book.ask:
      if ask.HasField('price'):
        price = ask.price
      else:
        price = ask.price_float
      self.asks.append(SidedBookMock(price, ask.qty_float))

    for bid in self.book_feed.book.bid:
      if bid.HasField('price'):
        price = bid.price
      else:
        price = bid.price_float
      self.bids.append(SidedBookMock(price, bid.qty_float))

  def as_proto_int(self):
    # TODO: verify it has int
    return self.book_feed.book

  def as_proto_float(self):
    # TODO: verify it has float
    return self.book_feed.book


class Price(object):
  def __init__(self, ask_price, bid_price):
    self.ask_price = float(ask_price)
    self.bid_price = float(bid_price)
    self.mid_price = float((ask_price + bid_price) / 2.0)

  def __str__(self):
    return 'ask_price:%.8f, bid_price:%.8f, mid_price:%.8f' % (
        self.ask_price, self.bid_price, self.mid_price)


class BookHolder(metaclass=abc.ABCMeta):
  ProductType = None
  is_price_float = False

  def __init__(self, product):
    assert issubclass(self.ProductType, (NativeProduct, NativeFuturesProduct))

    self.product_request = product
    self.product = self.ProductType.FromAny(product)
    assert isinstance(self.product, (NativeProduct, NativeFuturesProduct))

    self.request_time = None
    self.fetched_time = None
    self.exchange_time = None
    self.query_result = None

  def set_book_none(self):
    self.request_time = None
    self.fetched_time = None
    self.exchange_time = None
    self.query_result = None

  def set_book_data(self, request_time, fetched_time, obj):
    # request_time, fetched_time must be a result of time.time()
    assert type(request_time) == float
    assert type(fetched_time) == float
    self.request_time = _if_not_none_nano(request_time)
    self.fetched_time = _if_not_none_nano(fetched_time)
    self.exchange_time = None
    self.query_result = obj

  def set_book_proto(self, feed):
    self.request_time = feed.request_time
    self.fetched_time = feed.fetched_time
    if feed.HasField("exchange_time"):
      self.exchange_time = feed.exchange_time
    else:
      self.exchange_time = None
    self.query_result = ProtoMsgAdapter(feed)

  def _get_price_by_qty_impl(self, qty, side_book):
    sum_qty = 0
    for elem in side_book:
      sum_qty += float(elem.quantity)
      if sum_qty >= qty:
        return float(elem.price)
    raise Exception('Cannot find price with qty:%d' % qty)

  def _get_price_by_notional_impl(self, notional, side_book):
    sum_notional = 0
    for elem in side_book:
      sum_notional += float(elem.quantity) * float(elem.price)
      if sum_notional >= notional:
        return float(elem.price)
    raise Exception('Cannot find price with notional:%d' % notional)

  def _get_best_price_impl(self, side_book):
    return float(side_book[0].price)

  def get_price_by_qty(self, qty):
    ask_price = self._get_price_by_qty_impl(qty, self.query_result.asks)
    bid_price = self._get_price_by_qty_impl(qty, self.query_result.bids)
    return Price(ask_price, bid_price)

  def get_price_by_notional(self, notional):
    if self.query_result is None:
      return None
    ask_price = self._get_price_by_notional_impl(notional, self.query_result.asks)
    bid_price = self._get_price_by_notional_impl(notional, self.query_result.bids)
    return Price(ask_price, bid_price)

  def get_best_price(self):
    if self.query_result is None:
      return None
    ask_price = self._get_best_price_impl(self.query_result.asks)
    bid_price = self._get_best_price_impl(self.query_result.bids)
    return Price(ask_price, bid_price)

  def get_proto_feed(self):
    if self.query_result is None:
      return None
    return proto_pb2.FeedMessage(
        feed_type=proto_pb2.BOOK_FEED,
        feeder_id=0,
        machine_id=0,
        exchange_type=enums_pb2.ExchangeType.Value(self.product.exchange),
        symbol=str(self.product),
        fetched_time=self.fetched_time,
        request_time=self.request_time,
        book=self.query_result.as_proto_int(),
    )

  def get_proto_feed_float(self):
    if self.query_result is None:
      return None
    return proto_pb2.FeedMessage(
        feed_type=proto_pb2.BOOK_FEED,
        feeder_id=0,
        machine_id=0,
        exchange_type=enums_pb2.ExchangeType.Value(self.product.exchange),
        symbol=str(self.product),
        fetched_time=self.fetched_time,
        request_time=self.request_time,
        book=self.query_result.as_proto_float(),
    )


class FeedBase(BookHolder):
  def __init__(self, product):
    BookHolder.__init__(self, product)

  @abc.abstractmethod
  def query_book(self):
    raise NotImplementedError()


class TypedFeedBase(FeedBase):
  ProductType = None
  PublicClientType = None

  def __init__(self, product):
    FeedBase.__init__(self, product)
    self.pub_client = self.PublicClientType()

  def query_book(self):
    obj = self.pub_client.query_level_book(self.product)
    self.set_book_data(obj.req_timestamp, obj.res_timestamp, obj.msg)
